package com.sleekbyte.tailor.output; import com.sleekbyte.tailor.common.Location; import com.sleekbyte.tailor.common.Rules; import com.sleekbyte.tailor.common.Severity; import com.sleekbyte.tailor.format.Formatter; import com.sleekbyte.tailor.utils.Pair; import org.fusesource.jansi.Ansi; import org.fusesource.jansi.AnsiConsole; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * Generates and outputs formatted analysis messages for Xcode. */ public final class Printer implements Comparable<Printer> { private File inputFile; private Severity maxSeverity; private Formatter formatter; private Map<String, ViolationMessage> msgBuffer = new HashMap<>(); private Set<Pair<Integer, Integer>> ignoredRegions = new HashSet<>(); private boolean shouldPrintParseErrorMessage = false; /** * Constructs a printer for the specified input file, maximum severity, and color setting. * * @param inputFile The source file to verify * @param maxSeverity The maximum severity of any emitted violation messages * @param formatter Format to print in */ public Printer(File inputFile, Severity maxSeverity, Formatter formatter) { this.inputFile = inputFile; this.maxSeverity = maxSeverity; this.formatter = formatter; } /** * Prints warning message. * * @param rule Rule associated with warning * @param warningMsg Warning message to print * @param location Location object containing line and column number for printing */ public void warn(Rules rule, String warningMsg, Location location) { print(rule, Severity.WARNING, warningMsg, location); } // Use this method to print non rule based messages. public void warn(String warningMsg, Location location) { print(Severity.WARNING, warningMsg, location); } /** * Prints error message. * * @param rule Rule associated with error * @param errorMsg Error message to print * @param location Location object containing line and column number for printing */ public void error(Rules rule, String errorMsg, Location location) { print(rule, Severity.min(Severity.ERROR, maxSeverity), errorMsg, location); } // Visible for testing only public static String genOutputStringForTest(Rules rule, String filePath, int line, Severity severity, String msg) { return new ViolationMessage(rule, filePath, line, 0, severity, msg).toString(); } // Visible for testing only public static String genOutputStringForTest(String filePath, int line, int column, Severity severity, String msg) { return new ViolationMessage(filePath, line, column, severity, msg).toString(); } // Visible for testing only public static String genOutputStringForTest(Rules rule, String filePath, int line, int column, Severity severity, String msg) { return new ViolationMessage(rule, filePath, line, column, severity, msg).toString(); } public List<ViolationMessage> getViolationMessages() { return new ArrayList<>(this.msgBuffer.values()); } /** * Calls formatter to display all violation or error messages. * * @throws IOException if formatter cannot retrieve canonical path from inputFile */ public void printAllMessages() throws IOException { if (shouldPrintParseErrorMessage) { printParseErrorMessage(); } else { List<ViolationMessage> outputList = getViolationMessages().stream() .filter(this::shouldDisplayViolationMessage).collect(Collectors.toList()); Collections.sort(outputList); formatter.displayViolationMessages(outputList, inputFile); } } public long getNumErrorMessages() { return getNumMessagesWithSeverity(Severity.ERROR); } public long getNumWarningMessages() { return getNumMessagesWithSeverity(Severity.WARNING); } /** * Suppress analysis output for a given region. * * @param start line number where the region begins * @param end line number where the region ends */ public void ignoreRegion(int start, int end) { this.ignoredRegions.add(new Pair<>(start, end)); } public void setShouldPrintParseErrorMessage(boolean shouldPrintError) { shouldPrintParseErrorMessage = shouldPrintError; } /** * Print all rules along with their descriptions to STDOUT. */ public static void printRules() { Rules[] rules = Rules.values(); AnsiConsole.out.println(Ansi.ansi().render(String.format("@|bold %d rules available|@%n", rules.length))); for (Rules rule : rules) { AnsiConsole.out.println(Ansi.ansi().render(String.format("@|bold %s|@%n" + "@|underline Description:|@ %s%n" + "@|underline Style Guide:|@ %s%n", rule.getName(), rule.getDescription(), rule.getLink()))); } } @Override public int compareTo(Printer printer) { return this.inputFile.compareTo(printer.inputFile); } @Override public boolean equals(Object printerObject) { if (!(printerObject instanceof Printer)) { return false; } return this.inputFile.equals(((Printer) printerObject).inputFile); } @Override public int hashCode() { assert false : "hashCode not designed"; return 100; } private void print(Severity severity, String msg, Location location) { ViolationMessage violationMessage = new ViolationMessage(location.line, location.column, severity, msg); addToMsgBuffer(violationMessage); } private void print(Rules rule, Severity severity, String msg, Location location) { ViolationMessage violationMessage = new ViolationMessage(rule, location.line, location.column, severity, msg); addToMsgBuffer(violationMessage); } private void addToMsgBuffer(ViolationMessage violationMessage) { try { violationMessage.setFilePath(this.inputFile.getCanonicalPath()); } catch (IOException e) { System.err.println("Error in getting canonical path of input file: " + e.getMessage()); } this.msgBuffer.put(violationMessage.toString(), violationMessage); } private void printParseErrorMessage() throws IOException { formatter.displayParseErrorMessage(inputFile); } private long getNumMessagesWithSeverity(Severity severity) { return msgBuffer.values().stream() .filter(this::shouldDisplayViolationMessage) .filter(msg -> msg.getSeverity().equals(severity)).count(); } private boolean shouldDisplayViolationMessage(ViolationMessage msg) { for (Pair<Integer, Integer> ignoredRegion : ignoredRegions) { if (ignoredRegion.getFirst() <= msg.getLineNumber() && msg.getLineNumber() <= ignoredRegion.getSecond()) { return false; } } return true; } }